package com.yc.snack.util;

import com.jspsmart.upload.File;
import com.jspsmart.upload.Files;
import com.jspsmart.upload.Request;
import com.jspsmart.upload.SmartUpload;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.jsp.PageContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * 请求参数工具类
 */
public class RequestParamUtil {
    /**
     * 将请求中的参数转成cls对象返回
     * @param cls
     * @param request
     * @param <T>
     * @return
     */
    public static <T> T getParams(Class<T> cls, HttpServletRequest request) {
        T t = null;

        try {
            // 1、获取请求中的所有参数名 name = value
            Enumeration<String> names = request.getParameterNames();

            // 2、获取给定类中的所有setter方法
            Method[] methods = cls.getDeclaredMethods();
            Map<String, Method> setters = new HashMap<String, Method>();
            String methodName = null;
            for(Method method : methods) {
                methodName = method.getName(); // 获取当前方法的方法名
                if(methodName.startsWith("set")) { // 如果方法名是set开头，则认为是一个setter
                    setters.put(methodName, method);
                }
            }

            // 3、根据请求中的参数从setter方法中查找对应的方法 -> name值的第一个字母大写前面加上set   setName();
            String name = null;
            Method method = null;
            Class<?>[] types = null;
            Class<?> type = null;

            t = cls.newInstance(); // 实例化对象
            String value = null;
            while(names.hasMoreElements()) { // 循环所有的name名
                name = names.nextElement(); // 从枚举中获取下一个name值
                methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);

                // 根据方法名从setters中查找这个处理方法
                method = setters.get(methodName);
                if(method == null) {
                    continue;
                }

                // 如果有这个方法，为了能够正确的激活方法注值，需要现获取这个方法的形参类型
                types = method.getParameterTypes(); // 获取方法的形参列表类型
                if(types == null || types.length <= 0) { // 说明这个方法没有形参列表，则没法注值
                    continue;
                }

                // 如果有，规范的setter只能有一个参数，对应一个属性
                type = types[0];
                value = request.getParameter(name); // 先获取这个参数的值

                if(Integer.class == type) { // 如果形参类型是整型，说明是Integer
                    method.invoke(t, Integer.valueOf(value));
                } else if(Integer.TYPE == type) {
                    method.invoke(t, Integer.parseInt(value));
                } else if(Double.class == type) {
                    method.invoke(t, Double.valueOf(value));
                } else if(Double.TYPE == type) {
                    method.invoke(t, Double.parseDouble(value));
                } else if(Long.class == type) {
                    method.invoke(t, Long.valueOf(value));
                } else if(Long.TYPE == type) {
                    method.invoke(t, Long.parseLong(value));
                } else {
                    method.invoke(t, value);
                }
            }
            // 4、激活对象中的这个方法注值
            // (1) 先实例化一个这样的对象 newInstance();
            // (2) method.invoke(obj, args) -> 先获取这个方法的形参类型
            // (3) 从请求中根据name获取这个形参值，转成这个方法需要的类型
            // (4) invoke()
            // (5) 所有属性注值完成后，返回这个对象
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        return t;
    }

    // 允许上传的文件类型
    private static final String ALLOWLIST = "gif,jpg,png,jpeg,doc,docx,xls,xlsx,txt,zip,rar,mp3,mp4";
    private static final int MAXFILESIZE = 10 * 1024 * 1024; // 单个文件的最大大小10M
    private static final int TOTALMAXSIZE = 100 * 1024 * 1024; // 每次请求的最大总大小

    /**
     * 处理带文件上传的请求
     * @param pageContext
     * @return map 以表单元素name属性的值为键，以文件在服务器中的地址为值
     */
    public static Map<String, String> upload(PageContext pageContext) throws Exception{
        Map<String, String> result = new HashMap<String, String>();

        // 1、实例化组件
        SmartUpload smartUpload = new SmartUpload();

        // 2、初始化组件
        smartUpload.initialize(pageContext);

        // 3、设置文件上传的参数：编码集、文件大小、允许或拒绝上传的文件类型等
        smartUpload.setCharset("utf-8"); // 编码集
        smartUpload.setAllowedFilesList(ALLOWLIST); // 允许上传的文件后缀列表
        smartUpload.setMaxFileSize(MAXFILESIZE); // 单个文件的最大值
        smartUpload.setTotalMaxFileSize(TOTALMAXSIZE); // 每次请求的总大小

        // 4、上传文件
        smartUpload.upload(); // 读取请求中的数据流

        // 5、组件会将HttpServletRequest对象转成Request
        Request request = smartUpload.getRequest(); // 这个请求对象中存放的是普通表单元素，即除 type="file" 类型的

        // 6、处理普通的表单元素数据。此时，并不知道每次请求会有哪些参数过来，即name属性的值不知道
        Enumeration<String> names = request.getParameterNames();

        String name = null;
        // 循环根据name取值
        while(names.hasMoreElements()) {
            name = names.nextElement();
            result.put(name, request.getParameter(name));
        }

        // 7、处理上传的文件
        // 获取所有上传的文件
        Files files = smartUpload.getFiles();
        if(files == null || files.getCount() <= 0 || files.getSize() <= 0) { // 说明没有上传文件
            return result;
        }

        // 如果带文件则需要循环处理。注意：不是一个 input type="file" 中有多个文件，而是表单中有多个 type="file"
        Collection<File> fls = files.getCollection(); // 返回这个对象的集合表示，方便循环
        String fieldName = null; // 表单中name属性的值
        String filePath = null; // 文件保存路径
        String temp = null;
        String fileStr = ""; // 做字符串拼接，存储一个文本框中有多个文件的时候的路径，中间用;隔开

        for(File f1 : fls) {
            if(!f1.isMissing()) { // 如果文件上传过程中没有丢失数据
                temp = f1.getFieldName();

                if (StringUtil.checkNull(fieldName)) { // 说明此时读取的是第一个文件
                    fieldName = temp;
                } else { // 说明不是第一个文件，则需要判断是不是跟上一个文件属于同一个文本框
                    if (!temp.equals(fieldName)) { // 说明换了一个文本框
                        result.put(fieldName, fileStr);
                        fileStr = "";
                        fieldName = temp; // fieldName指向新的文本框的值
                    }
                }

                // 处理文件上传 -> 重命名文件，避免文件重名
                filePath = ConstantInfo.uploadpath + "/" + System.currentTimeMillis() + "_" + f1.getFileName();

                // 上传，将文件存到指定的路径，这个路径必须是绝对路径
                f1.saveAs(ConstantInfo.basepath + "/" + filePath);

                // 文字水印
                WaterMarkUtil.textWaterMark(new java.io.File(ConstantInfo.basepath + "/" + filePath));

                // 图片水印
                //WaterMarkUtil.picWaterMark(new java.io.File(ConstantInfo.basepath + "/" + filePath));

                if(StringUtil.checkNull(fileStr)) {
                    fileStr = filePath;
                } else {
                    fileStr += ";" + filePath;
                }
            }
        }

        result.put(fieldName, fileStr); // 处理最后一个文本框中的数据
        return result;
    }

    /**
     * 处理带文件上传的请求（基于对象的封装）
     * @param pageContext
     * @return map 以表单元素name属性的值为键，以文件在服务器中的地址为值
     */
    public static <T> T upload(Class<T> cls, PageContext pageContext) throws Exception{
        // 实例化对象
        T t = cls.newInstance();

        // 1、实例化组件
        SmartUpload smartUpload = new SmartUpload();

        // 2、初始化组件
        smartUpload.initialize(pageContext);

        // 3、设置文件上传的参数：编码集、文件大小、允许或拒绝上传的文件类型等
        smartUpload.setCharset("utf-8"); // 编码集
        smartUpload.setAllowedFilesList(ALLOWLIST); // 允许上传的文件后缀列表
        smartUpload.setMaxFileSize(MAXFILESIZE); // 单个文件的最大值
        smartUpload.setTotalMaxFileSize(TOTALMAXSIZE); // 每次请求的总大小

        // 4、上传文件
        smartUpload.upload(); // 读取请求中的数据流

        // 5、组件会将HttpServletRequest对象转成Request
        Request request = smartUpload.getRequest(); // 这个请求对象中存放的是普通表单元素，即除 type="file" 类型的

        // 6、处理普通的表单元素数据。此时，并不知道每次请求会有哪些参数过来，即name属性的值不知道
        Enumeration<String> names = request.getParameterNames();
        String name = null;

        // 根据请求中的name，将对应的值注入到 t 对应的属性中 -> 获取这个类中的所有setter方法
        Method[] methods = cls.getDeclaredMethods();
        Map<String, Method> setter = new HashMap<String, Method>();

        // 从所有方法中提取出setter方法 -> 以set开头的方法
        for(Method method : methods) {
            name = method.getName();
            if(name.startsWith("set")) {
                setter.put(name, method);
            }
        }

        // 注值
        String methodName = null;
        Method method = null;
        Class<?>[] types = null;
        Class<?> type = null;
        String value = null;
        while(names.hasMoreElements()) {
            name = names.nextElement();

            methodName = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
            // 从setter方法中根据方法名获取方法
            method = setter.get(methodName);

            if(method == null) { // 如果找不到对应的set方法，说明这个属性没法赋值，则不管
                continue;
            }

            // 获取这个方法的形参类型
            types = method.getParameterTypes();
            if(types == null || types.length <= 0) { // 说明这个setter方法没有提供形参，则这个属性也无法注值
                continue;
            }

            // 标准的set方法应该只有一个形参
            type = types[0]; // 取出第一个形参类型
            value = request.getParameter(name);
            if(Integer.class == type) { // 如果形参类型是整型，说明是Integer
                method.invoke(t, Integer.valueOf(value));
            } else if(Integer.TYPE == type) {
                method.invoke(t, Integer.parseInt(value));
            } else if(Double.class == type) {
                method.invoke(t, Double.valueOf(value));
            } else if(Double.TYPE == type) {
                method.invoke(t, Double.parseDouble(value));
            } else if(Long.class == type) {
                method.invoke(t, Long.valueOf(value));
            } else if(Long.TYPE == type) {
                method.invoke(t, Long.parseLong(value));
            } else { // 其余的都当成字符串处理，这里可能会有问题
                method.invoke(t, value);
            }
        }

        // 注意：虽然表单里面是文件，但是实体类中一般定义为String，因为存在数据库端的是这个文件在服务器的地址，而并不是文件信息
        // 7、处理上传的文件
        // 获取所有上传的文件
        Files files = smartUpload.getFiles();
        if(files == null || files.getCount() <= 0 || files.getSize() <= 0) { // 说明没有上传文件
            return t;
        }

        // 如果带文件则需要循环处理。注意：不是一个 input type="file" 中有多个文件，而是表单中有多个 type="file"
        Collection<File> fls = files.getCollection(); // 返回这个对象的集合表示，方便循环
        String fieldName = null; // 表单中name属性的值
        String filePath = null; // 文件保存路径
        String temp = null;
        String fileStr = ""; // 做字符串拼接，存储一个文本框中有多个文件的时候的路径，中间用;隔开

        for(File f1 : fls) {
            if(!f1.isMissing()) { // 如果文件上传过程中没有丢失数据
                temp = f1.getFieldName();

                if (StringUtil.checkNull(fieldName)) { // 说明此时读取的是第一个文件
                    fieldName = temp;
                } else { // 说明不是第一个文件，则需要判断是不是跟上一个文件属于同一个文本框
                    if (!temp.equals(fieldName)) { // 说明换了一个文本框
                        methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
                        method = setter.get(methodName);
                        if(method == null) {
                            continue;
                        }

                        // 正常情况下，这个属性的类型应该是字符串，所以这里省略了形参类型的判断
                        method.invoke(t, fileStr);
                        fileStr = "";
                        fieldName = temp; // fieldName指向新的文本框的值
                    }
                }

                // 处理文件上传 -> 重命名文件，避免文件重名
                filePath = ConstantInfo.uploadpath + "/" + System.currentTimeMillis() + "_" + f1.getFileName();

                // 上传，将文件存到指定的路径，这个路径必须是绝对路径
                f1.saveAs(ConstantInfo.basepath + "/" + filePath);

                // 文字水印
//                WaterMarkUtil.textWaterMark(new java.io.File(ConstantInfo.basePath + "/" + filePath));

                // 图片水印
                WaterMarkUtil.picWaterMark(new java.io.File(ConstantInfo.basepath + "/" + filePath));

                if(StringUtil.checkNull(fileStr)) {
                    fileStr = filePath;
                } else {
                    fileStr += ";" + filePath;
                }
            }
        }

        // 处理最后一个文本框中的数据
        methodName = "set" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
        method = setter.get(methodName);
        if(method == null) {
            return t;
        }
        // 正常情况下，这个属性应该是字符串，这里省略了形参类型判断
        method.invoke(t, fileStr);
        return t;
    }
}
